package org.datascooter.bundle;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Column;

import org.datascooter.bundle.attribute.AbstractAttribute;
import org.datascooter.bundle.attribute.ContainerAttribute;
import org.datascooter.bundle.attribute.DBIndex;
import org.datascooter.bundle.attribute.PersistAttribute;
import org.datascooter.bundle.attribute.PersistChildAttribute;
import org.datascooter.bundle.attribute.PersistReferenceAttribute;
import org.datascooter.bundle.attribute.Reference;
import org.datascooter.exception.DataScooterException;
import org.datascooter.exception.EntityNotMappedException;
import org.datascooter.generator.ContextIdGenerator;
import org.datascooter.generator.UUIDGenerator;
import org.datascooter.impl.Condition;
import org.datascooter.impl.DataSnip;
import org.datascooter.impl.DataSnipFactory;
import org.datascooter.impl.LogicValue;
import org.datascooter.impl.Snip;
import org.datascooter.inface.IDataManager;
import org.datascooter.inface.IFlushable;
import org.datascooter.inface.IIdGenerator;
import org.datascooter.utils.DBType;
import org.datascooter.utils.DSSettings;
import org.datascooter.utils.LangUtils;
import org.datascooter.utils.SnipUtils;
import org.datascooter.utils.TypeUtils;
import org.datascooter.utils.policy.SnipType;

/**
 * Purposed for describe entity and it's mapping to a database table. By default entity is a class name, but each class can be mapped many times if you set unique entity for them
 * 
 * @author nemo
 * 
 */
public class EntityBundle {

	private String tablePrefix = DSSettings.getTablePrefix();
	private String columnPrefix = DSSettings.getColumnPrefix();
	public boolean useSequence = DSSettings.useSequence();
	public String table;
	final String schema;
	public PersistAttribute id;
	public Condition discriminator;
	private String suffix = "";
	IIdGenerator<?> idGenerator;
	public String parentEntity;
	public PersistAttribute[] attrArray;
	public String[] attrNameArray;
	private boolean dbSchema;
	boolean isVolatile = false;
	public boolean cross = false;
	public boolean isUncheckable = false;
	public boolean hasPersistableChild = false;
	public boolean hasChild;
	public boolean hasContainers = false;
	public boolean hasEmbedded = false;
	public boolean hasEmbeddedEntities = false;
	public boolean hasParts = false;
	public boolean embeddable = false;
	public final Class<?> clazz;
	public final String entity;
	public final Map<String, AbstractAttribute> refMethodNameMap = new HashMap<String, AbstractAttribute>();
	public final Map<String, PersistAttribute> columnAttribute = new HashMap<String, PersistAttribute>();
	public final Map<String, PersistAttribute> columnAttributeMap = new HashMap<String, PersistAttribute>();
	public final Map<String, PersistAttribute> attributeNameMap = new HashMap<String, PersistAttribute>();
	public final Map<String, PersistReferenceAttribute> refEntityMap = new HashMap<String, PersistReferenceAttribute>();
	public final List<ContainerAttribute> containerAttributeList = new ArrayList<ContainerAttribute>();
	public final Map<String, ContainerAttribute> containerMap = new HashMap<String, ContainerAttribute>();
	public final List<DBIndex> indexList = new ArrayList<DBIndex>();
	public final List<EntityBundle> childList = new ArrayList<EntityBundle>();
	public final List<PersistChildAttribute> persistableChildList = new ArrayList<PersistChildAttribute>();
	public final List<ContainerAttribute> embeddedList = new ArrayList<ContainerAttribute>();
	public final List<EntityBundle> embeddedEntityList = new ArrayList<EntityBundle>();
	public final List<EntityPart> partList = new ArrayList<EntityPart>();
	public final Map<String, Integer> columnIndexMap = new HashMap<String, Integer>();
	public final List<PersistAttribute> attributeList = new ArrayList<PersistAttribute>();
	public final HashSet<String> attributeSet = new HashSet<String>();
	public final List<PersistReferenceAttribute> refList = new ArrayList<PersistReferenceAttribute>(0);

	public EntityBundle(String schema, boolean dbSchema) {
		this.schema = schema;
		this.table = null;
		this.entity = schema + "_" + dbSchema;
		this.dbSchema = dbSchema;
		clazz = null;
	}

	public EntityBundle(String name, String schema, String table, String klass, boolean uncheckable, boolean dbSchema) {
		this.dbSchema = dbSchema;
		if (klass != null) {
			try {
				clazz = Class.forName(klass);
			} catch (ClassNotFoundException e) {
				throw new DataScooterException("***Class not found  " + klass);
			}
		} else {
			clazz = null;
		}
		this.entity = LangUtils.isEmptyString(name) ? klass : name;
		this.schema = schema;
		if (dbSchema) {
			this.table = LangUtils.isEmptyString(schema) ? table : schema + "." + table;
		} else {
			this.table = table;
		}
		isUncheckable = uncheckable;
		if (this.entity == null) {
			throw new DataScooterException("***Null entity");
		}
	}

	public EntityBundle(String name, String schema, String table, boolean uncheckable, boolean dbSchema, Class<?> klass) {
		this.dbSchema = dbSchema;
		clazz = klass;
		this.entity = name;
		this.schema = schema;
		if (dbSchema) {
			this.table = LangUtils.isEmptyString(schema) ? table : schema + "." + table;
		} else {
			this.table = table;
		}
		isUncheckable = uncheckable;
		if (this.entity == null) {
			throw new DataScooterException("***Null entity");
		}
	}

	public void complete() {
		if (isCross() && !DSSettings.getUseIdInCrossTables()) {
			id = null;
		}
		List<PersistAttribute> list = new ArrayList<PersistAttribute>(attributeList);
		if (id != null) {
			if (list.contains(id)) {
				list.remove(id);
			}
			list.add(0, id);
		}
		attrArray = new PersistAttribute[list.size()];
		attrNameArray = new String[list.size()];
		int index = 0;
		for (PersistAttribute attribute : list) {
			attrArray[index] = attribute;
			attrNameArray[index] = attribute.name;
			columnIndexMap.put(attribute.getColumnName(), index);
			index++;
		}
	}

	public void changeAttributes(DBType dbType, EntityBundle bundle) throws SecurityException, NoSuchMethodException {
		if (id != null) {
			bundle.setId(id.copyMod(dbType));
			for (PersistAttribute attribute : attributeList) {
				bundle.addPersistAttribute((PersistAttribute) attribute.copy());
			}
			for (AbstractAttribute attribute : refMethodNameMap.values()) {
				if (attribute instanceof PersistReferenceAttribute) {
					bundle.refMethodNameMap.put(attribute.getActorName(), ((PersistReferenceAttribute) attribute).copyMod(dbType));
				} else {
					bundle.refMethodNameMap.put(attribute.getActorName(), attribute);
				}
			}
		}
	}

	public int getColumnIndex(String column) {
		return columnIndexMap.get(column);
	}

	public EntityBundle createChild(String name, String tableIn, String klass, boolean uncheckable) throws SecurityException, NoSuchMethodException {
		String tableChild = LangUtils.isEmptyString(tableIn) ? this.table : tableIn;
		return addChild(new EntityBundle(name, this.schema, tableChild, klass, uncheckable, this.dbSchema));
	}

	public EntityBundle createPart(String name, String tableIn) {
		String tableChild = LangUtils.isEmptyString(tableIn) ? this.table : tableIn;
		EntityBundle entityBundle = new EntityBundle(name, this.schema, tableChild, this.isUncheckable, this.dbSchema, this.clazz);
		entityBundle.setTablePrefix(tablePrefix);
		entityBundle.setColumnPrefix(columnPrefix);
		entityBundle.setSuffix(suffix);
		return entityBundle;
	}

	public Object[] flush(Object object, IDataManager dataManager) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException, InstantiationException, ParseException {
		if (IFlushable.class.isAssignableFrom(clazz)) {
			return ((IFlushable) object).flush();
		}
		Object[] result = new Object[attrArray.length];
		for (int a = 0; a < attrArray.length; a++) {
			result[a] = attrArray[a].getValue(object);
		}
		if (id != null && result[0] == null) {
			result[0] = provideId(object, dataManager);
		}
		return result;
	}

	public Object provideId(Object obj, IDataManager dataManager) {
		Object idValue = null;
		try {
			if (id != null) {
				idValue = id.getValue(obj);
				if (idValue == null) {
					if (useSequence) {
						idValue = dataManager.getIdManager().getId(entity);
					} else {
						if (idGenerator != null) {
							idValue = idGenerator.getId(obj);
						}
					}
					id.setValue(obj, idValue);
				}
			}
		} catch (Exception e) {
			throw new DataScooterException(e);
		}
		return idValue;
	}

	@SuppressWarnings("unchecked")
	public <T> T restore(Snip snip, Object[] objects) throws InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException, ParseException {
		T result = (T) clazz.newInstance();
		if (IFlushable.class.isAssignableFrom(clazz)) {
			((IFlushable) result).restore(objects);
		} else {
			int min = Math.min(snip.fields.length, objects.length);
			for (int a = 0; a < min; a++) {
				if (objects[a] != null) {
					AbstractAttribute attribute = columnAttribute.get(snip.fields[a]);
					if (attribute != null) {
						attribute.setValue(result, objects[a]);
					}
				}
			}
		}
		return result;
	}

	public PersistAttribute getAttribute(String attribute) {
		PersistAttribute simpleAttribute = attributeNameMap.get(attribute);
		if (simpleAttribute == null) {
			for (EntityBundle child : embeddedEntityList) {
				simpleAttribute = child.getAttribute(attribute);
				if (simpleAttribute != null) {
					return simpleAttribute;
				}
			}
			// throw new DataManagerException("***Not found attribute for " + LangUtils.defaultIfEmpty(attribute, "--Emty attribute-") + "  in bundle  " + getEntity());
		}
		return simpleAttribute;
	}

	public PersistAttribute getAttributeByColumn(String column) {
		return columnAttribute.get(column);
	}

	public String[] buildColumnsInt(DataSnip snip) {
		Set<String> columnSet = new HashSet<String>();
		List<String> result = new ArrayList<String>();
		SnipType snipType = snip.getSnipType();
		if (snip.extraList.size() > 0) {
			excludeAttributes(snip, columnSet, result, snip.excludeExtraList);
		} else {
			for (PersistAttribute attribute : attrArray) {
				addAttr(snipType, columnSet, result, attribute);
			}
		}
		for (EntityBundle embeded : embeddedEntityList) {
			for (PersistAttribute attribute : embeded.attrArray) {
				addAttr(snipType, columnSet, result, attribute);
			}
		}
		return result.toArray(new String[result.size()]);
	}

	public String[] buildColumnsInt(SnipType type) {
		Set<String> columnSet = new HashSet<String>();
		List<String> result = new ArrayList<String>();
		for (PersistAttribute attribute : attrArray) {
			addAttr(type, columnSet, result, attribute);
		}
		for (EntityBundle embeded : embeddedEntityList) {
			for (PersistAttribute attribute : embeded.attrArray) {
				addAttr(type, columnSet, result, attribute);
			}
		}
		return result.toArray(new String[result.size()]);
	}

	private void excludeAttributes(DataSnip snip, Set<String> columnSet, List<String> result, boolean exclude) {
		for (PersistAttribute attribute : attrArray) {
			if (snip.extraList.contains(attribute.name)) {
				if (exclude) {
					continue;
				}
			} else {
				if (!exclude) {
					continue;
				}
			}
			addAttr(snip.getSnipType(), columnSet, result, attribute);
		}
	}

	private void addAttr(SnipType type, Set<String> columnSet, List<String> result, PersistAttribute attribute) {
		if (columnSet.add(attribute.getColumnName())) {
			if ((type.equals(SnipType.INSERT) && attribute.isInsertable())) {
				result.add(attribute.getColumnName());
			} else if ((type.equals(SnipType.UPDATE) && attribute.isUpdatable())) {
				result.add(attribute.getColumnName());
			} else if (!type.equals(SnipType.UPDATE) && !type.equals(SnipType.INSERT)) {
				result.add(attribute.getColumnName());
			}
		}
	}

	private void provideMethods(AbstractAttribute attribute) throws SecurityException, NoSuchMethodException {
		if (clazz != null) {
			attribute.explore(clazz);
		}
	}

	/**
	 * Adds simple attribute
	 * 
	 * @param attribute
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws MappingException
	 */
	public void addPersistAttribute(PersistAttribute attribute) throws SecurityException, NoSuchMethodException {
		if (attributeSet.add(attribute.getColumn())) {
			if (id != null && id.equals(attribute)) {
				return;
			} else {
				addAttribute(attribute);
				attributeList.add(attribute);
			}
		}
	}

	private void addAttribute(PersistAttribute attribute) throws SecurityException, NoSuchMethodException {
		provideMethods(attribute);
		attribute.setColumnPrefix(columnPrefix);
		columnAttribute.put(attribute.getColumnName(), attribute);
		columnAttributeMap.put(attribute.getColumn(), attribute);
		attributeNameMap.put(attribute.name, attribute);
	}

	public void customizeAttribute(Column column, PersistAttribute attribute) {
		if (column.name().length() > 0 && !column.name().equals(attribute.getColumn())) {
			columnAttribute.remove(attribute.getColumnName());
			columnAttributeMap.remove(attribute.getColumn());
			attribute.setColumn(column.name());
			columnAttribute.put(attribute.getColumnName(), attribute);
			columnAttributeMap.put(attribute.getColumn(), attribute);
		}
		attribute.setScale(column.scale());
		attribute.setInsertable(column.insertable());
		attribute.setUpdatable(column.updatable());
		attribute.setNullable(column.nullable());
		attribute.setColumnDefinition(column.columnDefinition());
		if (column.unique()) {
			Iterator<DBIndex> iterator = indexList.iterator();
			while (iterator.hasNext()) {
				if (iterator.next().getName().equals(column.name())) {
					iterator.remove();
				}
			}
			addIndex(new DBIndex(column.name(), SnipUtils.UNIQUE, attribute));
		}
	}

	public void addReferenceAttribute(PersistReferenceAttribute attribute) throws SecurityException, NoSuchMethodException {
		addPersistAttribute(attribute);
		PersistReferenceAttribute put = refEntityMap.put(attribute.getEntity(), attribute);
		if (put != null) {
			throw new DataScooterException("***Lost attribute: " + attribute.getEntity() + "  " + attribute.name + "-----" + put.getColumn());
		}
		attribute.setBundle(this);
		addReferenceInfo(attribute);
	}

	public void addBackReference(String name, String entity, String attribute) throws SecurityException, NoSuchMethodException {
		Reference reference = new Reference(name);
		DataSnip dataSnip = DataSnipFactory.select(entity).where(attribute, "");
		dataSnip.where.valueList.get(0).setHook(this.entity, null, null);
		reference.setList(dataSnip);
		addReferenceInfo(reference);
	}

	public void addCrossReference(String name, String entity, String crossEntity, String joinAttribute, String joinFromChild) throws EntityNotMappedException, SecurityException, NoSuchMethodException {
		Reference reference = new Reference(name);
		if (joinAttribute != null) {
			DataSnip dataSnip = DataSnipFactory.select(entity);
			dataSnip.join(joinAttribute, crossEntity, LangUtils.isBooleanString(joinFromChild, false)).join(this.entity).where(id.name, "").where.valueList.get(0).setHook(entity, null, null);
			reference.setList(dataSnip);
		} else {
			DataSnip dataSnip = DataSnipFactory.select(entity);
			dataSnip.join(crossEntity).join(this.entity).where(id.name, "").where.valueList.get(0).setHook(entity, null, null);
			reference.setList(dataSnip);
		}
		addReferenceInfo(reference);
	}

	public void addReference(Reference reference) throws SecurityException, NoSuchMethodException {
		addReferenceInfo(reference);
	}

	private void addReferenceInfo(AbstractAttribute reference) throws SecurityException, NoSuchMethodException {
		if (clazz != null) {
			reference.explore(clazz);
			refMethodNameMap.put(reference.getActorName(), reference);
		}
	}

	/**
	 * Returns an attribute referred to given entity if it exists
	 * 
	 * @param entityName
	 * @return
	 */
	public PersistReferenceAttribute getEntityReference(String entityName) {
		return refEntityMap.get(entityName);
	}

	public AbstractAttribute getAttributeReference(String methodName) {
		return refMethodNameMap.get(methodName);
	}

	public void setId(PersistAttribute idIn) throws SecurityException, NoSuchMethodException {
		this.id = idIn;
		addAttribute(idIn);
		if (!attributeSet.add(idIn.getColumn())) {
			Iterator<PersistAttribute> iterator = attributeList.iterator();
			while (iterator.hasNext()) {
				if (iterator.next().getColumn().equalsIgnoreCase(idIn.getColumn())) {
					iterator.remove();
					break;
				}
			}
		}
	}

	public Object getId(Object obj) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		if (id == null) {
			return null;
		}
		return id.getValue(obj);
	}

	public void setDiscriminator(Condition discr) {
		this.discriminator = discr;
		processDiscriminatorTypes(discriminator);
	}

	private void processDiscriminatorTypes(Condition discr) {
		for (LogicValue logicValue : discr.valueList) {
			PersistAttribute attribute = (PersistAttribute) getAttribute(logicValue.attribute);
			switch (attribute.getType()) {
				case INT:
					logicValue.setValue(Integer.valueOf(logicValue.getValue() + ""));
					break;
				case BIGDECIMAL:
					logicValue.setValue(new BigDecimal(logicValue.getValue() + ""));
					break;
				case BOOLEAN:
					logicValue.setValue(Boolean.valueOf(logicValue.getValue() + ""));
					break;
				case LONG:
					logicValue.setValue(Long.valueOf(logicValue.getValue() + ""));
					break;
				case TIMESTAMP:
					try {
						logicValue.setValue(TypeUtils.parseDate(logicValue.getValue() + ""));
					} catch (ParseException e) {
						e.printStackTrace();
					}
					break;
			}
		}
		for (Condition condition : discr.childList) {
			processDiscriminatorTypes(condition);
		}
	}

	public void addIndex(DBIndex dbIndex) {
		indexList.add(dbIndex);
	}

	public void addPersistableChild(PersistChildAttribute child) throws SecurityException, NoSuchMethodException {
		if (clazz != null) {
			child.explore(clazz);
		}
		persistableChildList.add(child);
		hasPersistableChild = true;
	}

	public EntityBundle addChild(EntityBundle child) throws SecurityException, NoSuchMethodException {
		childList.add(child);
		child.setTablePrefix(tablePrefix);
		child.setColumnPrefix(columnPrefix);
		child.setSuffix(suffix);
		addParentAttributes(child);
		child.parentEntity = entity;
		hasChild = true;
		return child;
	}

	public EntityBundle addEmbeded(EntityBundle child) {
		embeddedEntityList.add(child);
		child.setColumnPrefix(columnPrefix);
		child.setTablePrefix(tablePrefix);
		child.setSuffix(suffix);
		child.parentEntity = entity;
		child.setEmbeddable(true);
		hasEmbeddedEntities = true;
		return child;
	}

	private void addParentAttributes(EntityBundle bundle) throws SecurityException, NoSuchMethodException {
		if (id != null) {
			bundle.setId((PersistAttribute) id.copy());
			bundle.setIdGenerator(idGenerator);
			for (PersistAttribute attribute : attributeList) {
				bundle.addPersistAttribute((PersistAttribute) attribute.copy());
			}
			for (PersistReferenceAttribute attribute : refEntityMap.values()) {
				bundle.addReferenceAttribute((PersistReferenceAttribute) attribute.copy());
			}
		}
	}

	public void setTablePrefix(String pref) {
		tablePrefix = LangUtils.defaultIfEmpty(pref, tablePrefix);
	}

	public void setSuffix(String suf) {
		suffix = LangUtils.defaultIfEmpty(suf);
	}

	public void setColumnPrefix(String columnPrefix) {
		this.columnPrefix = LangUtils.defaultIfEmpty(columnPrefix, this.columnPrefix);
	}

	public void setIdGenerator(IIdGenerator<?> idGenerator) {
		this.idGenerator = idGenerator;
		useSequence = useSequence && !(idGenerator instanceof UUIDGenerator) && !(idGenerator instanceof ContextIdGenerator);
	}

	public void addContainerAttribute(ContainerAttribute containerAttribute) throws SecurityException, NoSuchMethodException {
		if (clazz != null) {
			containerAttribute.explore(clazz);
		}
		if (containerAttribute.isEmbedded()) {
			embeddedList.add(containerAttribute);
			hasEmbedded = true;
		} else {
			this.containerAttributeList.add(containerAttribute);
			containerMap.put(containerAttribute.name, containerAttribute);
			hasContainers = true;
		}
	}

	public ContainerAttribute getContainerAttribute(String name) {
		return containerMap.get(name);
	}

	public void addPart(EntityPart entityPart) {
		partList.add(entityPart);
		hasParts = true;
	}

	public void setEmbeddable(boolean embeddable) {
		this.embeddable = embeddable;
		this.isUncheckable = embeddable;
	}

	public String getTableName() {
		return tablePrefix + table + suffix;
	}

	public String getColumnPrefix() {
		return columnPrefix;
	}

	@Override
	public String toString() {
		return "EntityBundle [table=" + table + ", entity=" + entity + "]";
	}

	/**
	 * Adds a referrer record - entity referred to this entity
	 * 
	 * @param bundle
	 * @param attribute
	 */
	public void addReferrer(PersistReferenceAttribute attribute) {
		refList.add(attribute);
	}

	/**
	 * Returns a list of entities referred to this entity
	 * 
	 * @return
	 */
	public List<PersistReferenceAttribute> getRefList() {
		return refList;
	}

	/**
	 * Returns a objectMap of entities this entity referred to
	 * 
	 * @return
	 */
	public Map<String, PersistReferenceAttribute> getRefEntityMap() {
		return refEntityMap;
	}

	public boolean isCross() {
		return refEntityMap.size() == attributeList.size();
	}
}
